
package at.fhj.ase.clientarch.server.component;

import java.io.FileInputStream;
import java.io.IOException;
import java.rmi.AccessException;
import java.rmi.AlreadyBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import at.fhj.ase.clientarch.common.component.IHardwareComponent;
import at.fhj.ase.clientarch.common.component.IUserComponent;
import at.fhj.ase.clientarch.common.exception.AlreadyInitializedException;
import at.fhj.ase.clientarch.common.util.CommonUtil;

/**
 * Class that is responsible for all the RMI stuff.<br>
 * It creates a registry and binds the components to it.
 * <p>
 * Note:<br>
 * If the registry cannot start, it throws a {@link RemoteException}.<br>
 * Every other exception <strong>must</strong> be caught outside.
 * 
 * @author cja, sfe | www.fh-joanneum.at | Client Architectures and Design
 */
public enum RMIController {
  INSTANCE;

  private final Logger logger = LoggerFactory.getLogger(getClass().getName());

  private IUserComponent userComponent;
  private IHardwareComponent hardwareComponent;

  /** The initial client config (empty at instantiation) */
  private final Properties clientConfig = new Properties();

  public void init() throws IOException {
    // Create and install a security manager to use the policy-file with its permissions and keep that system secure
    if (System.getSecurityManager() == null) {
      System.setSecurityManager(new RMISecurityManager());
    } else {
      throw new AlreadyInitializedException("RMIController has already been initialized");
    }

    // Create SSL-based registry
    final Registry registry = LocateRegistry.createRegistry(CommonUtil.RMI_PORT, new SslRMIClientSocketFactory(), new SslRMIServerSocketFactory());
    logger.info("RMI: registry created");

    initComponents(registry);
    readClientConfig();
  }

  /**
   * Bind containing object instances to the RMI-registry and makes it available for client(s).<br>
   * Note that this method does not stop the application if binding fails at known exceptions.<br>
   * If an unknown exception is thrown, the RMI-registry will also fail to register which lets the application <strong>stop</strong>.
   * 
   * @param registry the RMI registry where the containing components shell be bindet to
   * @throws RemoteException if the initialization of one of the components went wrong
   */
  private void initComponents(final Registry registry) throws RemoteException {
    //TODO may implement a list of components and create a single object with a name. All those objects in list are then dynamically added to RMI
    userComponent = new UserComponentImpl();
    hardwareComponent = new HardwareComponentImpl();

    String COMPONENT_NAME = IUserComponent.NAME;
    bindComponentToRegistry(registry, COMPONENT_NAME, userComponent);
    logger.info("RMI: Registered component " + UserComponentImpl.class.getName());

    COMPONENT_NAME = IHardwareComponent.NAME;
    bindComponentToRegistry(registry, COMPONENT_NAME, hardwareComponent);
    logger.info("RMI: Registered component " + HardwareComponentImpl.class.getName());
  }

  private void bindComponentToRegistry(final Registry registry, final String componentName, final Remote componentToBind) {
    try {
      registry.bind(componentName, componentToBind);
    } catch (AccessException e) {
      logger.error("Could not bind " + componentName + " to registry due to following error: ", e);
    } catch (RemoteException e) {
      logger.error("Could not bind " + componentName + " to registry due to following error: ", e);
    } catch (AlreadyBoundException e) {
      logger.error("Could not bind " + componentName + " to registry due to following error: ", e);
    } catch (NullPointerException e) {
      logger.error("Could not bind " + componentName + " to registry due to following error: ", e);
    }
  }

  /**
   * Saves the initial but also the current config for client.<br>
   * This method can be used also several times during server is running as we may want to change config values for clients that connect in the future/inbetween
   * without shutting the server down.<br>
   * Throws Exceptions if anything goes wrong which should <strong>shutdown server</strong> as this config is <strong>crucial for clients</strong>.
   * 
   * @throws IOException if the file could not be found or the stream could not be closed => server shutdown
   */
  private void readClientConfig() throws IOException {
    final FileInputStream fis = new FileInputStream("cfg/client-config.properties");
    clientConfig.load(fis);
    fis.close();
  }

  /**
   * Always tries to return the most current client config.<br>
   * If an exception occurs, it just logs it and returns the initial config read at server startup.<br>
   * We can be sure that this works because the server would not be started otherwise.
   * 
   * @return the <strong>must</strong> current client config (never <code>null</code>)
   */
  Properties getClientConfig() {
    // in case of a changed client config, we fetch it again. if an exception occurs, we are forced to return the initial one.
    try {
      readClientConfig();
    } catch (IOException e) {
      logger.error("Could not re-read client config, returning initial one...: ", e);
    }

    return clientConfig;
  }
}
